package com.xiyuan.smartutils;

import com.xiyuan.smartutils.constants.CodeConstants;
import com.xiyuan.smartutils.filters.Filter;
import com.xiyuan.smartutils.filters.FilterHandler;
import com.xiyuan.smartutils.loader.SystemLoader;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 资源相关工具类
 *
 * @version v1.0.0 @author lgz 2017-8-27 新建与整理
 */

@SuppressWarnings("SameParameterValue")
public final class Resources implements CodeConstants {
    
    private Resources() {
    }
    
    private static ClassLoader getLoader(Class<?> cls) {
        ClassLoader cl = cls != null ? cls.getClassLoader() : SystemLoader.getClassLoader();
        if (cl == null) {cl = Resources.class.getClassLoader();}
        return cl;
    }
    
    /**
     * 判断资源是否存在，资源路径，如/xxx/xxx/resource/conf/config.xml
     *
     * @param path 资源路径
     * @return true/false 存在则返回true
     */
    public static boolean exists(String path) {
        path = StrUtils.removeStartsWith(path, "/");
        return Thread.currentThread().getContextClassLoader().getResource(path) != null;
    }
    
    /**
     * 判断资源是否存在，资源路径，如/xxx/xxx/resource/conf/config.xml
     *
     * @param clazz 资源能加载到的类
     * @param path  资源路径
     * @return true/false 存在则返回true
     */
    public static boolean exists(Class<?> clazz, String path) {
        return exists(path) || clazz.getResource(path) != null;
    }
    
    /**
     * 获取资源URL，资源路径，如/xxx/xxx/resource/conf/config.xml
     *
     * @param path 资源路径
     * @return 得到资源URL
     */
    public static URL getResource(String path) {
        return getResource(null, path);
    }
    
    /**
     * 获取资源URL，资源路径，如/xxx/xxx/resource/conf/config.xml
     *
     * @param clazz 资源能加载到的类
     * @param path  资源路径
     * @return 得到资源URL
     */
    public static URL getResource(Class<?> clazz, String path) {
        path = StrUtils.removeStartsWith(path, "/");
        
        // 处理多版本jar
        int javaVersion = Systems.JAVA_MAIN_VERSION;
        for (int version = javaVersion; version >= 8; version--) {
            String versionedPath = "META-INF/versions/" + version + "/" + path;
            URL url = clazz != null ? clazz.getResource(versionedPath) : getLoader(null).getResource(versionedPath);
            if (url != null) {return url;}
        }
        
        return clazz != null ? clazz.getResource(path) : getLoader(null).getResource(path);
    }
    
    /**
     * 读取资源文件流 path格式为/xxx/xxx/example/res/abc.js
     * 自动处理多版本资源
     *
     * @param path 路径
     * @return InputStream
     */
    public static InputStream getResourceStream(String path) {
        path = StrUtils.removeStartsWith(path, "/");
        ClassLoader classLoader = getLoader(null);
        // 处理多版本jar
        int javaVersion = Systems.JAVA_MAIN_VERSION;
        for (int version = javaVersion; version >= 8; version--) {
            String versionedPath = "META-INF/versions/" + version + "/" + path;
            InputStream url = classLoader.getResourceAsStream(versionedPath);
            if (url != null) {return url;}
        }
        return classLoader.getResourceAsStream(path);
    }
    
    /**
     * 读取资源文件流 path格式为/xxx/xxx/example/res/abc.js
     * 自动处理多版本资源
     *
     * @param path 路径
     * @return byte[] , if not fund return null
     */
    public static byte[] getResourceBytes(String path) throws IOException {
        InputStream in = getResourceStream(path);
        return in == null ? null : IoUtils.getBytes(in);
    }
    
    /**
     * 读取资源文件流 path格式为/xxx/xxx/example/res/abc.js
     * 自动处理多版本资源
     *
     * @param path 路径
     * @return byte[] , if not fund return null
     */
    public static byte[] getResourceBytes(Class<?> clz, String path) {
        InputStream in = getResourceStream(clz, path);
        try {
            return in == null ? null : IoUtils.getBytes(in);
        }
        catch (IOException e) {
            return null;
        }
    }
    
    /**
     * 读取资源文件流 path格式为/xxx/xxx/example/res/abc.js
     *
     * @param clazz 类，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path  路径
     * @return InputStream
     */
    public static InputStream getResourceStream(Class<?> clazz, String path) {
        InputStream in = clazz.getResourceAsStream(path);
        return in != null ? in : getResourceStream(path);
    }
    
    /**
     * 读取资源文件 path格式为/xxx/xxx/httpd/context/service/resource/abc.js
     *
     * @param path 路径
     * @return 文件内容
     * @throws IOException 可能的异常
     */
    public static String getResourceStringUTF8(String path) throws IOException {
        return getResourceString(path, _UTF_8_);
    }
    
    /**
     * 读取资源文件 path格式为/xxx/xxx/httpd/context/service/resource/abc.js
     *
     * @param clazz 类名，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path  路径
     * @return 文件内容
     * @throws IOException 可能的异常
     */
    public static String getResourceStringUTF8(Class<?> clazz, String path) {
        return getResourceString(clazz, path, _UTF_8_);
    }
    
    /**
     * 读取资源文件，并将内容以字符串形式输出。 如果文件不存在，或路径错误，则返回空字符对象
     *
     * @param path     资源类对应的资源路径
     * @param encoding 编码
     * @return 文件内容
     */
    public static String getResourceString(String path, String encoding) {
        if (Validates.isEmpty(path) || Validates.isEmpty(encoding)) {return null;}
        try {
            byte[] b = IoUtils.getBytesClassPath(path);
            return b == null ? null : new String(b, encoding);
        }
        catch (IOException ex) {
            return null;
        }
    }
    
    /**
     * 读取资源文件，并将内容以字符串形式输出。 如果文件不存在，或路径错误，则返回空字符对象
     *
     * @param clazz    类，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path     资源类对应的资源路径
     * @param encoding 编码
     * @return 文件内容
     */
    public static String getResourceString(Class<?> clazz, String path, String encoding) {
        if (Validates.isEmpty(path) || Validates.isEmpty(encoding)) {return null;}
        
        try {
            byte[] b = IoUtils.getBytesClassPath(clazz, path);
            return b == null ? null : new String(b, encoding);
        }
        catch (IOException ex) {
            return null;
        }
    }
    
    /**
     * 读取指资源目录下的文件名列表，不作下级目录检查，非文件或不可读的不处理
     *
     * @param clazz 搜索对应的资源类，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path  搜索对应的路径
     * @return 读取指资源目录下的文件名列表
     * @throws IOException 可能的异常
     */
    public static List<String> getResourceFileNameList(Class<?> clazz, String path) throws IOException {
        return getResourceNameList(clazz, path, 1);
    }
    
    /**
     * 读取指资源目录下的目录名列表，不作下级目录检查，非目录或不可读的不处理，并过滤掉最后的/结尾
     *
     * @param clazz 搜索对应的资源类，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path  搜索对应的路径
     * @return 读取指资源目录下的文件名列表
     * @throws IOException 可能的异常
     */
    public static List<String> getResourceFolderNameList(Class<?> clazz, String path) throws IOException {
        List<String> folderList = getResourceNameList(clazz, path, 2);
        folderList.replaceAll(s -> StrUtils.trimRight(s, "/"));
        return folderList;
    }
    
    /**
     * 读取指资源目录下的文件和目录名列表，不作下级目录检查，不可读的不处理（注意目录以/结尾）
     *
     * @param clazz 搜索对应的资源类，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path  搜索对应的路径
     * @return 读取指资源目录下的文件名列表
     * @throws IOException 可能的异常
     */
    public static List<String> getResourceNameList(Class<?> clazz, String path) throws IOException {
        return getResourceNameList(clazz, path, 0);
    }
    
    /**
     * 读取指资源目录下的文件名或目录名列表，不作下级目录检查，指定类型0,1,2
     *
     * @param clazz 搜索对应的资源类，类和资源路径在同一JAR中，保证ClassLoader必能加载到
     * @param path  搜索对应的路径
     * @param type  类型，0表示目录和文件，1表示文件，2表示目录
     * @return 读取指资源目录下的文件名列表
     * @throws IOException 可能的异常
     */
    public static List<String> getResourceNameList(Class<?> clazz, String path, int type) throws IOException {
        path = StrUtils.removeStartsWith(path, "/");
        path = StrUtils.addEndsWith(path, "/");
        
        // 第一步，读取资源目录列表
        List<File> fileDirList = new ArrayList<File>();
        List<JarFile> jarDirList = new ArrayList<JarFile>();
        Enumeration<URL> resources = clazz.getClassLoader().getResources(path);
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            if ("file".equals(resource.getProtocol())) {// 目录
                fileDirList.add(new File(resource.getFile()));
            }
            else if ("jar".equals(resource.getProtocol())) {// JAR包
                String resPath = resource.getPath();
                int ind = resPath.indexOf("!");
                if (ind == -1) {
                    continue;
                }
                
                resPath = resPath.substring(0, ind);
                resPath = StrUtils.removeStartsWith(resPath, "file:");
                jarDirList.add(new JarFile(resPath));
            }
            else if ("bundleresource".equals(resource.getProtocol())) {// eclipse的bundle中的资源
                try {
                    Class<?> cls = Classes.forName("org.eclipse.core.runtime.FileLocator");
                    Method m = cls.getDeclaredMethod("toFileURL", URL.class);
                    resource = (URL) m.invoke(null, resource);
                    fileDirList.add(new File(resource.getPath()));
                }
                catch (Exception e) {// 异常表示不支持
                }
            }
            // 其他的暂不支持
        }
        
        // 第二步，从目录文件和JAR中搜索
        List<String> fileNameList = new ArrayList<String>();
        
        // 2.1先搜目录
        for (File fileDir : fileDirList) {
            File[] files = fileDir.listFiles();
            for (File file : files) {
                if (!file.isFile() && !file.canRead()) {
                    continue;
                }
                
                fileNameList.add(file.getName());
            }
        }
        
        // 2.2再搜JAR
        for (JarFile fileDir : jarDirList) {
            Enumeration<JarEntry> enumeration = fileDir.entries();
            while (enumeration.hasMoreElements()) {
                JarEntry entry = enumeration.nextElement();
                String name = entry.getName();
                if (!name.startsWith(path)) {
                    continue;// 不是该目录的不要
                }
                
                name = StrUtils.removeStartsWith(name, path);
                if (_EMPTY_.equals(name)) {
                    continue;// 当前目录不要
                }
                
                switch (type) {
                    case 0: {// 文件+目录
                        if (name.indexOf("/") == -1) {
                            fileNameList.add(name);
                        }
                        else if (StrUtils.getTimes(name, '/') == 1 && StrUtils.endsWith(name, "/")) {
                            fileNameList.add(name);
                        }
                        break;
                    }
                    case 1: {// 文件
                        if (name.indexOf("/") == -1) {
                            fileNameList.add(name);
                        }
                        break;
                    }
                    case 2: {// 目录，要求/只有一个且在最后
                        if (StrUtils.getTimes(name, '/') == 1 && StrUtils.endsWith(name, "/")) {
                            fileNameList.add(name);
                        }
                        break;
                    }
                    default:
                        break;
                }
            }
        }
        return fileNameList;
    }
    
    /**
     * 生成静态属性，要求静态属性名和配置的properties名称相同
     *
     * @param clazz 类结构
     */
    public static void buildStaticProperties(Class<?> clazz) {
        buildStaticProperties(clazz, clazz.getName());
    }
    
    /**
     * 生成静态属性，要求静态属性名和配置的properties名称相同
     *
     * @param clazz          类结构
     * @param propertiesPath 属性文件类路径，格式(xxx.xxx.example.Clazz)不含后缀.properties
     */
    public static void buildStaticProperties(Class<?> clazz, String propertiesPath) {
        ResourceBundle bundle = ResourceBundle.getBundle(propertiesPath, Locale.getDefault(), clazz.getClassLoader());
        for (String name : bundle.keySet()) {
            Field field = Classes.getField(clazz, name);
            if (field == null) {
                continue;
            }
            
            Classes.setFieldValue(null, field, bundle.getString(name));
        }
    }
    
    /**
     * 新建对象，生成属性，要求属性名和配置的properties名称相同
     *
     * @param clazz 类结构
     */
    public static <T> T buildProperties(Class<T> clazz) {
        return buildProperties(clazz, clazz.getName());
    }
    
    /**
     * 新建对象，生成属性，要求属性名和配置的properties名称相同
     *
     * @param cls            类结构
     * @param propertiesPath 属性文件类路径，格式(xxx.xxx.example.Clazz)不含后缀.properties
     */
    public static <T> T buildProperties(Class<T> cls, String propertiesPath) {
        ResourceBundle bundle = ResourceBundle.getBundle(propertiesPath, Locale.getDefault(), cls.getClassLoader());
        
        T obj = Classes.newInstance(cls);
        for (String name : bundle.keySet()) {
            Field field = Classes.getField(cls, name);
            if (field == null) {
                continue;
            }
            
            Classes.setFieldValue(obj, field, bundle.getString(name));
        }
        
        return obj;
    }
    
    
    /**
     * 扫描指定类路径，根据提供的过滤器和处理器处理
     *
     * @param filter   过滤器
     * @param handler  处理器
     * @param pathList 指定类路径
     * @return 是否扫描成功
     */
    public static boolean scanClassPath(Filter<String> filter, FilterHandler handler, List<String> pathList) {
        for (String path : pathList) {
            File file = new File(path);
            if (!Files.exists(file) || !file.canRead() || (!file.isFile() && !file.isDirectory())) {continue;}
            
            if (file.isDirectory()) {
                // .class目录
                try {
                    List<File> fileList = new ArrayList<File>();
                    Files.filter(file, (r) -> filter.accept(file.getPath()), fileList::add);
                    for (File classFile : fileList) {
                        if (!handler.handle(new Object[]{file, classFile})) {return false;}
                    }
                }
                catch (Throwable e) {
                    continue;
                }
            }
            else if (file.isFile() && (StrUtils.endsWithIgnoreCase(path, ".jar") || StrUtils.endsWithIgnoreCase(path,
                                                                                                                ".zip"))) {// jar/zip文件
                try {
                    JarFile jarFile = new JarFile(path);
                    List<JarEntry> entryList = Jars.filter(jarFile, filter);
                    
                    for (JarEntry jarEntry : entryList) {
                        if (!handler.handle(new Object[]{jarFile, jarEntry})) {return false;}
                    }
                }
                catch (Throwable e) {
                    continue;
                }
            }
        }
        
        return true;
    }
}
